Skip to main content

4.3 - Understanding Units - The Unit Attributes

Every single entity on the battlefield—from a Marine to a mineral patch, from a Pylon to a flying Overlord—is represented in your code as a Unit object. This is the most fundamental data structure in python-sc2. It is both a data container and an actor: it holds all the information about an entity, and it is the object you use to issue it commands.

Mastering the Unit object is non-negotiable. It is how you will read the battlefield, manage your base, and control your army.

A Mental Model: The Unit as a Class

Think of every unit in the game as an instance of a class. The object holds the unit's current state and provides methods to change that state.

       +------------------------------------+
| Unit (e.g., Marine) |
|====================================|
| -- Properties -- | <-- Data you can READ
| name: "Marine" |
| health: 45 |
| position: Point2(34.5, 67.5) |
| is_idle: True |
| ... |
|------------------------------------|
| -- Methods -- | <-- Actions you can CALL
| + attack(target) |
| + move(position) |
| + hold_position() |
| ... |
+------------------------------------+

How to Get a Unit Object

You will almost never create a Unit object manually. Instead, you will retrieve them from the game state using the helper properties on self.

# Get a single Unit object representing your first townhall.
my_townhall = self.townhalls.first

# Get a single Unit object representing a random idle worker.
idle_worker = self.workers.idle.random_or(None)

# Loop through a collection of Unit objects.
for marine in self.units(UnitTypeId.MARINE):
# 'marine' is a Unit object.
if marine.is_idle:
print(f"Found an idle marine with tag {marine.tag}")

Key Unit Attributes: A Developer's Reference

Once you have a Unit object, you can inspect its attributes to make decisions.

CategoryAttributeData TypeUse Case in a Project
Identityname
type_id
tag
str
UnitTypeId
int
type_id is best for checks. Use tag to uniquely identify a unit throughout a match.
Vitalshealth, health_max
shield, shield_max
energy, energy_max
floatCheck health to decide if a unit should retreat. Check energy to see if a caster can use an ability.
Positionposition
radius
Point2
float
position is essential for all movement and distance calculations.
Combatweapon_cooldown
is_attacking
float
bool
weapon_cooldown is the key to advanced micro-management like stutter-stepping.
State Flagsis_idle
is_flying
is_powered
is_burrowed
boolThese booleans are the most common conditions in if statements for managing unit behavior.
Ownershipis_yours
is_enemy
boolCrucial for distinguishing your units from the opponent's.

A Developer's Checklist for Using a Unit

When your code gets a Unit object, this is your thought process:

  • 1. Identify It: What is this unit? (if unit.type_id == UnitTypeId.MARINE:)
  • 2. Check Its State: Is it doing what it should be? (if unit.is_idle:)
  • 3. Assess Its Condition: Is it healthy enough to fight? (if (unit.health / unit.health_max) < 0.4:)
  • 4. Give It an Order: If it's not doing the right thing, issue a command (unit.attack(...)).

Code Example: The Field Commander Bot

This bot demonstrates the full "Sense-Think-Act" cycle on individual Unit objects. It finds idle Marines and sends them to attack, and pulls back any Marine that is badly damaged.

# field_commander_bot.py

from sc2.main import run_game
from sc2 import maps
from sc2.bot_ai import BotAI
from sc2.ids.unit_typeid import UnitTypeId
from sc2.player import Bot, Computer
from sc2.data import Difficulty, Race


class FieldCommanderBot(BotAI):
"""
Implements a basic Terran strategy focused on Marine production and simple combat micro.

This bot's architecture is divided by functional responsibility:
- Economy (building workers)
- Production (building structures and training units)
- Army Management (controlling military units)
"""

async def on_step(self, iteration: int):
"""
Main entry point for bot logic, executed on each game step.
Orchestrates high-level functions in a specific order.
"""
# The first action is always to make sure our workers are busy.
# 'await' is used because these functions can take time to execute.
await self.distribute_workers()
await self.manage_production()
await self.manage_army()

# =================================================================
# PRODUCTION & ECONOMY
# =================================================================

async def manage_production(self):
"""Coordinates the building of workers, structures, and units."""
await self.build_workers()
await self.build_supply_depots()
await self.build_barracks()
await self.train_marines()

async def build_workers(self):
"""Trains SCVs up to a saturation point of 16."""
# The `already_pending` check prevents queuing multiple units at once.
if (
self.townhalls.ready
and self.can_afford(UnitTypeId.SCV)
and self.supply_workers < 16
and self.already_pending(UnitTypeId.SCV) == 0
):
self.townhalls.ready.random.train(UnitTypeId.SCV)

async def build_supply_depots(self):
"""Prevents getting supply-blocked by building depots when supply is low."""
if (
self.supply_left < 5
and self.can_afford(UnitTypeId.SUPPLYDEPOT)
and self.already_pending(UnitTypeId.SUPPLYDEPOT) == 0
):
# `build` automatically finds a worker and a valid build location.
await self.build(UnitTypeId.SUPPLYDEPOT, near=self.townhalls.ready.random)

async def build_barracks(self):
"""Builds a single Barracks once a Supply Depot is complete."""
# Check prerequisite: a completed Supply Depot must exist.
if not self.structures(UnitTypeId.SUPPLYDEPOT).ready.exists:
return

# Limit to one Barracks for this simple strategy.
if (
not self.structures(UnitTypeId.BARRACKS).exists
and self.can_afford(UnitTypeId.BARRACKS)
and self.already_pending(UnitTypeId.BARRACKS) == 0
):
await self.build(UnitTypeId.BARRACKS, near=self.townhalls.ready.random)

async def train_marines(self):
"""Trains Marines from any idle Barracks."""
for barracks in self.structures(UnitTypeId.BARRACKS).ready.idle:
if self.can_afford(UnitTypeId.MARINE) and self.supply_left > 0:
barracks.train(UnitTypeId.MARINE)

# =================================================================
# ARMY MANAGEMENT
# =================================================================

async def manage_army(self):
"""Coordinates all army movements and actions."""
target = self.find_target()
await self.command_marines(target)

def find_target(self):
"""
Determines a target for the army.
Prioritizes visible enemy structures, falls back to enemy start location.
"""
if self.enemy_structures.exists:
return self.enemy_structures.random.position
return self.enemy_start_locations[0]

async def command_marines(self, target):
"""Iterates through all Marines and issues an individual command."""
for marine in self.units(UnitTypeId.MARINE):
self.command_individual_marine(marine, target)

def command_individual_marine(self, marine, target):
"""
Applies micro-management logic to a single Marine.
"""
# Retreat if health is below 50%
if marine.health / marine.health_max < 0.5:
# The 'move' command is for retreating, as units will not stop to fight.
marine.move(self.start_location)
# Attack if idle and not retreating
elif marine.is_idle:
# The 'attack' command engages any enemies encountered on the way to the target.
marine.attack(target)


# =================================================================
# LAUNCHER
# =================================================================
if __name__ == "__main__":
run_game(
maps.get("AbyssalReefLE"),
[Bot(Race.Terran, FieldCommanderBot()), Computer(Race.Zerg, Difficulty.Easy)],
realtime=True,
)